#include "GLWin32Viewer.h"
#include "GL.h"

#include <Rz/Collections/Dictionary.h>
#include <Rz/Graphics/Render/ViewerEvents.h>
#include <Rz/Diagnostics/Assert.h>
#include <Rz/String.h>
#include <Rz/Range.h>
#include <Rz/Memory.h>

namespace Rz { namespace Graphics { namespace Render { namespace Libraries { namespace OpenGL {

Collections::Dictionary<HWND, GLWin32Viewer*> _GLWindowContainer;

static const PIXELFORMATDESCRIPTOR _pixelformatDescriptor =
{
	sizeof(PIXELFORMATDESCRIPTOR),
	1,
	PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER,
	PFD_TYPE_RGBA,
	32,
	0, 0, 0, 0, 0, 0, 0, 0,
	0, 0, 0, 0, 0,
	32,             // Z-buffer.
	0,              // Stencil buffer.
	0,
	PFD_MAIN_PLANE,
	0, 0, 0, 0
};

bool GLWin32Viewer::Init(const ViewerState& state)
{
	u32				pixelFormat;
	DWORD			dwExStyle, dwStyle;
	WNDCLASS		wc  = { 0 };
	RECT			rec = { 0 };

	Memory::Set(&_wnd, 0, sizeof(_wnd));

	_wnd.hInstance = GetModuleHandle(0);

	SetViewerState(state);

	wc.style         = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
	wc.lpfnWndProc   = &(::__GLWin32Viewer_WndProc);
	wc.hInstance     = _wnd.hInstance;
	wc.lpszClassName = TEXT("GLWin32viewer");

	//RZ_ASSERT
		(RegisterClass(&wc));

	if(state.Fullscreen)
	{
		DEVMODE dmScreenSettings = { 0 };

		dmScreenSettings.dmSize       = sizeof(DEVMODE);
		dmScreenSettings.dmFields     = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;
		dmScreenSettings.dmBitsPerPel = 32;
		dmScreenSettings.dmPelsWidth  = static_cast<DWORD>(state.ViewerSize.GetWidth());
		dmScreenSettings.dmPelsHeight = static_cast<DWORD>(state.ViewerSize.GetHeight());

		if (ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN) != DISP_CHANGE_SUCCESSFUL)
		{
			return false;
		}

		dwExStyle = WS_EX_APPWINDOW;
		dwStyle   = WS_VISIBLE | WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;

		while (ShowCursor(FALSE) >= 0);	// hide cursor
	}
	else
	{
		dwExStyle = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
		dwStyle   = WS_VISIBLE | WS_CAPTION | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_SYSMENU;
	}

	rec.left   = 0;
	rec.top    = 0;
	rec.right  = static_cast<LONG>(state.ViewerSize.GetWidth());
	rec.bottom = static_cast<LONG>(state.ViewerSize.GetHeight());

	AdjustWindowRect(&rec, dwStyle, 0);

	_wnd.hWnd = CreateWindowEx(dwExStyle, wc.lpszClassName, wc.lpszClassName, dwStyle,
		((GetSystemMetrics(SM_CXSCREEN) - rec.right + rec.left) / 2),
		((GetSystemMetrics(SM_CYSCREEN) - rec.bottom + rec.top) / 2),
		rec.right - rec.left,
		rec.bottom - rec.top,
		0, 0, _wnd.hInstance, this);

	if (this->GetRenderDevice())
	{
		this->GetRenderDevice()->Init();
	}

	// TODO: viewer init?
	RZ_ASSERT(_wnd.hWnd != 0);

	_wnd.hDC = GetDC(_wnd.hWnd);
	RZ_ASSERT(_wnd.hDC != 0);

	pixelFormat = ChoosePixelFormat(_wnd.hDC, &_pixelformatDescriptor);
	RZ_ASSERT(pixelFormat);

	SetPixelFormat(_wnd.hDC, pixelFormat, &_pixelformatDescriptor);

	_wnd.hRC = wglCreateContext(_wnd.hDC);
	RZ_ASSERT(_wnd.hRC != 0);

	// TODO: separate context manipulation into RenderDevice
	wglMakeCurrent(_wnd.hDC, _wnd.hRC);

	GL::Init();
	//GL::SwapInterval(0);

	return true;
}

void GLWin32Viewer::End()
{
	if (_wnd.hRC)
	{
		wglMakeCurrent(0, 0);
		wglDeleteContext(_wnd.hRC);
	}

	if (_wnd.hDC)
	{
		ReleaseDC(_wnd.hWnd, _wnd.hDC);
	}

	if (_wnd.hWnd)
	{
		DestroyWindow(_wnd.hWnd);
	}

	UnregisterClass(TEXT("GLWin32viewer"), _wnd.hInstance);

	if (this->GetViewerState().Fullscreen)
	{
		ChangeDisplaySettings(0, 0);
		while (ShowCursor(1) < 0); // show cursor
	}
}

void GLWin32Viewer::Run()
{
	bool running = true;
	int frames = 0;

	RZ_ASSERT(this->GetViewerHandler());

	bool viewerInitStatus = this->GetViewerHandler()->Init();

	RZ_ASSERT(viewerInitStatus);

	_startTime = timeGetTime();

	while (running)
	{
		if (!ProcessMessage(&running))
		{
			f32 frameTime   = 0.001f * static_cast<f32>(timeGetTime() - _frameTime);
			f32 elapsedTime = 0.001f * static_cast<f32>(timeGetTime() - _startTime);

			if (GetActiveWindow() == _wnd.hWnd)
			{
				ProcessMouseInput();
				ProcessKeyboardInput();
			}
			else
			{
				this->SetActiveStatus(false);
			}

			_frameTime = timeGetTime();

			running = this->GetViewerHandler()->Update(elapsedTime, frameTime);

			SwapBuffers(_wnd.hDC);

			DrawTime(elapsedTime, frameTime);

			frames++;
		}
	}
}

LRESULT GLWin32Viewer::WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
		case WM_SYSCOMMAND:
			if (wParam == SC_SCREENSAVE || wParam == SC_MONITORPOWER)
			{
				return 0;
			}
			break;

		case WM_CLOSE:
		case WM_DESTROY:
			PostQuitMessage(0);
			return 0;

		case WM_CHAR:
			if (wParam == VK_ESCAPE)
			{
				PostQuitMessage(0);
				return 0;
			}
			break;
	}

	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

bool GLWin32Viewer::ProcessMessage(bool* runFlag)
{
	static MSG msg;

	if (runFlag)
	{
		*runFlag = true;
	}

	if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
	{
		if (runFlag && msg.message == WM_QUIT)
		{
			*runFlag = false;
		}

		TranslateMessage(&msg);
		DispatchMessage(&msg);

		return true;
	}
	else
	{
		return false;
	}
}

void GLWin32Viewer::ProcessMouseInput()
{
	using namespace Events;

	POINT p;
	GetCursorPos(&p);

	if (!this->IsActive())
	{
		this->GetEventInfo()->Mouse.DX = 0;
		this->GetEventInfo()->Mouse.DY = 0;
		this->SetActiveStatus(true);
	}
	else
	{
		this->GetEventInfo()->Mouse.DX = p.x - this->GetEventInfo()->Mouse.X;
		this->GetEventInfo()->Mouse.DY = p.y - this->GetEventInfo()->Mouse.Y;
	}

	this->GetEventInfo()->Mouse.X = p.x;
	this->GetEventInfo()->Mouse.Y = p.y;

	bool mbl = this->GetEventInfo()->Mouse.Buttons[MouseButton::Left];
	bool mbr = this->GetEventInfo()->Mouse.Buttons[MouseButton::Right];

	this->GetEventInfo()->Mouse.Buttons[MouseButton::Left]  = !!(GetAsyncKeyState(VK_LBUTTON) & 0x8000);
	this->GetEventInfo()->Mouse.Buttons[MouseButton::Right] = !!(GetAsyncKeyState(VK_RBUTTON) & 0x8000);

	MouseButton button =     this->GetEventInfo()->Mouse.Buttons[MouseButton::Left]  ?
		MouseButton::Left  : this->GetEventInfo()->Mouse.Buttons[MouseButton::Right] ?
		MouseButton::Right : MouseButton::None;

	if (mbl != this->GetEventInfo()->Mouse.Buttons[MouseButton::Left])
	{
		if (mbl)
		{
			this->GetViewerHandler()->OnMouseUp(MouseButton::Left, p.x, p.y);
		}
		else
		{
			this->GetViewerHandler()->OnMouseDown(MouseButton::Left, p.x, p.y);
		}
	}
	else if (mbr != this->GetEventInfo()->Mouse.Buttons[MouseButton::Right])
	{
		if (mbl)
		{
			this->GetViewerHandler()->OnMouseUp(MouseButton::Right, p.x, p.y);
		}
		else
		{
			this->GetViewerHandler()->OnMouseDown(MouseButton::Right, p.x, p.y);
		}
	}

	if (this->GetEventInfo()->Mouse.DX || this->GetEventInfo()->Mouse.DY)
	{
		this->GetViewerHandler()->OnMouseMove(button, p.x, p.y, this->GetEventInfo()->Mouse.DX, this->GetEventInfo()->Mouse.DY);
	}
}

void GLWin32Viewer::ProcessKeyboardInput()
{
	using namespace Events;

	bool tmp[256];
	Memory::Copy(&tmp, &(this->GetEventInfo()->Key), 256);

	this->GetEventInfo()->Key[Key::ArrowLeft]  = !!(GetAsyncKeyState(VK_LEFT)		& 0x8000);
	this->GetEventInfo()->Key[Key::ArrowRight] = !!(GetAsyncKeyState(VK_RIGHT)		& 0x8000);
	this->GetEventInfo()->Key[Key::ArrowUp]    = !!(GetAsyncKeyState(VK_UP)		& 0x8000);
	this->GetEventInfo()->Key[Key::ArrowDown]  = !!(GetAsyncKeyState(VK_DOWN)		& 0x8000);
	this->GetEventInfo()->Key[Key::PageUp]     = !!(GetAsyncKeyState(VK_PRIOR)		& 0x8000);
	this->GetEventInfo()->Key[Key::PageDown]   = !!(GetAsyncKeyState(VK_NEXT)		& 0x8000);
	this->GetEventInfo()->Key[Key::Space]      = !!(GetAsyncKeyState(VK_SPACE)		& 0x8000);
	this->GetEventInfo()->Key[Key::RightShift] = !!(GetAsyncKeyState(VK_RSHIFT)	& 0x8000);
	this->GetEventInfo()->Key[Key::RightCtrl]  = !!(GetAsyncKeyState(VK_RCONTROL)	& 0x8000);
	this->GetEventInfo()->Key[Key::LeftShift]  = !!(GetAsyncKeyState(VK_LSHIFT)	& 0x8000);
	this->GetEventInfo()->Key[Key::LeftCtrl]   = !!(GetAsyncKeyState(VK_LCONTROL)	& 0x8000);

	for (Key i : Range<Key>(Key::Num0, Key::Num9))
	{
		this->GetEventInfo()->Key[i] = !!(GetAsyncKeyState(static_cast<int>(i)) & 0x8000);
	}

	for (Key i : Range<Key>(Key::A, Key::Z))
	{
		this->GetEventInfo()->Key[i] = !!(GetAsyncKeyState(static_cast<int>(i)) & 0x8000);
	}

	for (u32 i : Range<>(255))
	{
		if (tmp[i] && tmp[i] == this->GetEventInfo()->Key[i])
		{
			this->GetViewerHandler()->OnKeyDown(Key(char(i & 0xFF)), this->GetEventInfo()->Mouse.X, this->GetEventInfo()->Mouse.Y);
		}
		else if (!tmp[i] && tmp[i] != this->GetEventInfo()->Key[i])
		{
			this->GetViewerHandler()->OnKeyUp(Key(char(i & 0xFF)), this->GetEventInfo()->Mouse.X, this->GetEventInfo()->Mouse.Y);
		}
	}
}

void GLWin32Viewer::DrawTime(f32 elapsedTime, f32 frameTime)
{
	static u32      frame = 0;
	static f32		to    = 0.0f;
	static u32      fps   = 0;
	static TCHAR	str[256];
	u32             s, m, c;

	if (elapsedTime < 0.0f || this->GetViewerState().Fullscreen)
	{
		return;
	}

	frame++;
	if ((elapsedTime - to) > 1.0f)
	{
		fps = frame;
		to = elapsedTime;
		frame = 0;
	}

	if (!(frame & 3))
	{
		m = static_cast<u32>(elapsedTime / 60.0f);
		s = static_cast<u32>(elapsedTime - 60.0f * static_cast<f32>(m));
		c = static_cast<u32>(elapsedTime * 100.0f) % 100;

		TString::CFormat(str, RZ_COUNT_OF(str), "%02d:%02d:%02d [%d fps] [frametime: %i ms]", m, s, c, fps, static_cast<s32>(frameTime * 1000.0f));
		SetWindowText(_wnd.hWnd, str);
	}
}

} } } } }

LRESULT CALLBACK __GLWin32Viewer_WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	Rz::Graphics::Render::Libraries::OpenGL::GLWin32Viewer* viewer = nullptr;

	if (hWnd)
	{
		switch (uMsg)
		{
			case WM_CREATE:
				{
					LPCREATESTRUCT cs = ((LPCREATESTRUCT)lParam);

					viewer = reinterpret_cast<Rz::Graphics::Render::Libraries::OpenGL::GLWin32Viewer*>(cs->lpCreateParams);
					Rz::Graphics::Render::Libraries::OpenGL::_GLWindowContainer[hWnd] = viewer;
				}
				break;

			default:
				viewer = Rz::Graphics::Render::Libraries::OpenGL::_GLWindowContainer[hWnd];
				break;
		}
	}

	if (viewer)
	{
		return viewer->WndProc(hWnd, uMsg, wParam, lParam);
	}

	return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
